/*
 * @Description: 参数签名
 * @Author: Haor
 * @Date: 2022-05-07 11:15:10
 * @LastEditTime: 2022-08-17 21:33:03
 * @LastEditors: 罗梦豪 757986850@qq.com
 */
import { ExecutionContext, Injectable, CanActivate } from '@nestjs/common'
import { Reflector } from '@nestjs/core'
import { AppId } from '@constants/appid.constant'
import { GlobalConfig } from '@config/global'
import { SignException } from '@exceptions'
import { and } from '@utils/base'
import { Logger } from '@utils/log4'
import { SignParams, SignUtil } from "@libs/param.sign";
import { Request } from 'express'
import { CacheService } from "@cache";

/**
 * NOTE:
 * 1、接口是否需要签名访问？
 * 2、如果需要签名，取出签名，解析签名（1、是否过期、2、随机数是否重复、3、参数是否变化）
 * header中参数包含：
 * appId：
 * timestamp：时间戳
 * nonce：10为随机数
 * sign：（业务参数排序后 + appid + timestamp + nonce）生成的签名参数
 */
@Injectable()
export class ParamSignGuard implements CanActivate {
	constructor(
	    private reflector: Reflector,
        private cacheService: CacheService
    ) {}
	async canActivate(context: ExecutionContext): Promise<boolean> {
	    // const user = await this.cacheService.get('user')
		//  NOTE: 是否开启参数签名
		if (!GlobalConfig.isUseSign) {
			return true
		}
		//	NOTE: 获取自定义装饰器信息
		const noParamSign = this.reflector.get<string[]>('no-sign', context.getHandler())
		if (noParamSign) {
			return true
		}

		// NOTE: 获取请求信息
		const request = context.switchToHttp().getRequest() //获取请求信息
		const { appid, timestamp, nonce, sign } = request.headers


		// NOTE: 验证appid是否正确
		if (appid !== AppId) {
			Logger.error('签名appid错误')
			throw new SignException()
		}

		// NOTE: header中几个参数，缺一不可
		if (!and(appid, timestamp, nonce, sign)) {
			Logger.error('签名参数传递有误！')
			throw new SignException()
		}

		// NOTE: 判断当前日期是否在范围内
		if (!this.validateTimestamp(timestamp)) {
			Logger.error('签名时间超过服务器限制')
			throw new SignException()
		}

		// NOTE: 验证相同noce的请求是否已经存在，存在表示为重复请求
		if (!this.validateNonce(nonce)) {
			Logger.error('签名重复的请求')
			throw new SignException()
		}

		const businessParams = this.getBusinessParams(request) // 业务参数参数
        Logger.log('获取到的参数：', businessParams)
        Logger.log('获取到的Sign：', sign)

		const validateSignRes = this.validateSign(
            businessParams,
			{
				appId: appid,
				timestamp: timestamp,
				nonce: nonce
			},
			sign
		)

		// NOTE: 服务器生成签名与header中签名对比
		if (!validateSignRes) {
            Logger.error('错误的签名信息')
			throw new SignException()
		}
		return true
	}

	// 请求随机数组
	private noncePool = []

	/**
	 * 验证时间戳
	 * @param {Number} reqTime 请求时间
	 * @return {Boolean}
	 */
	private validateTimestamp(reqTime: number): boolean {
		const MAX_TIME = GlobalConfig.signExpirationTime * 1000
		return Date.now() - reqTime < MAX_TIME
	}

	/**
	 * 验证随机数
	 * @param {Number} nonce 随机字符
	 * @return {Boolean}
	 */
	private async validateNonce(nonce: number | string): Promise<boolean> {
        const CACHE_NONCE_KEY = 'auth_sign_nonce'
        let noncePool = await this.cacheService.get(CACHE_NONCE_KEY) //获取签名的key

        // 如果缓存为空，则初始化缓存
        if (!noncePool || !noncePool.length) {
            await this.cacheService.set(CACHE_NONCE_KEY, [nonce])
            return true
        }

        noncePool = JSON.parse(noncePool)

        //当前随机数在缓存中存在
		if (noncePool.includes(nonce)) return false
		noncePool.push(nonce)
        await this.cacheService.set(CACHE_NONCE_KEY, noncePool)
        return true
	}

    /**
     * 验证签名
     * @param {Object} businessParams 业务参数
     * @param {SignParams} signParams 签名参数
     * @param {String} sign 前端传递的签名字符
     * @return {Boolean}
     * @private
     */
	private validateSign(businessParams: object, signParams: SignParams, sign: string) {
		// 初始化签名工具
		const signUtil = new SignUtil(businessParams, signParams)
		const signStr = signUtil.getSign()
		Logger.log('接口生成的Sign>>>:', signStr)
		// NOTE: 服务器生成签名与header中签名对比
		return signUtil.validate(sign)
	}

    /**
     * 获取业务参数
     */
    private getBusinessParams(req: Request) {
        const { method } = req
        let params = {}
        switch (method) {
            case 'POST':
                params = req.body
                break;
            case 'GET':
                params = req.query
                break;
            default:
                params = {}
                break;
        }
        return { ...req.params, ...params}
    }
}
